/* Copyright (c) 2022 渝州大数据实验室
 *
 * Lanius is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *
 *     http://license.coscl.org.cn/MulanPSL2
 *
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.yzbdl.lanius.orchestrate.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.yzbdl.lanius.orchestrate.common.dto.system.OprLogFieldLabelDto;
import org.yzbdl.lanius.orchestrate.common.entity.system.UserLogEntity;
import org.yzbdl.lanius.orchestrate.serv.handle.system.FieldLabelHandle;
import org.yzbdl.lanius.orchestrate.serv.service.system.UserLogService;
import org.yzbdl.lanius.orchestrate.common.annotation.log.OprLabel;
import org.yzbdl.lanius.orchestrate.common.annotation.log.OprLog;
import org.yzbdl.lanius.orchestrate.common.annotation.log.OprId;
import org.yzbdl.lanius.orchestrate.common.utils.SpringUtil;

import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
 * 操作日志
 *
 * @author chenjunhao@yzbdl.ac.cn
 * @date 2022-04-13 15:13
 */
@Aspect
@Component
public class OprLogAspect {


	@Autowired
	UserLogService userLogService;

	private final String ID_NAME = "id";

	@Pointcut("@annotation(org.yzbdl.lanius.orchestrate.common.annotation.log.OprLog)")
	public void controllerAspect(){}

	@Around("controllerAspect()")
	public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
		HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
		if(!isIgnore(request)){
			OprLog oprLog = getAnoFromJoinPoint(joinPoint);
			if(oprLog!=null){
				String eventName = oprLog.value();
				String eventFormatMsg = oprLog.msg();
				String eventContent = assembleEventContentFromJoinPoint(joinPoint,eventFormatMsg);
				if(!oprLog.before()){
					Object res = joinPoint.proceed(joinPoint.getArgs());
					insertLog(eventContent,eventName,request);
					return res;
				}
				insertLog(eventContent,eventName,request);
			}
		}
		return joinPoint.proceed(joinPoint.getArgs());
	}

	/**
	 * 在某些请求下绕过日志记录
	 * 正常情况下不会出现上下文无用户
	 * @param request 请求对象
	 * @return 是否
	 */
	private boolean isIgnore(HttpServletRequest request){
		return false;
	}

	/**
	 * 获取当前的方法
	 * @param joinPoint 切入点
	 * @return 方法类
	 */
	private Method getMethodFromJoinPoint(ProceedingJoinPoint joinPoint){
		MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
		return methodSignature.getMethod();
	}

	/**
	 * 获取当前的操作注解
	 * @param joinPoint 加入点对象
	 * @return 注解类
	 */
	private OprLog getAnoFromJoinPoint(ProceedingJoinPoint joinPoint){
		return getMethodFromJoinPoint(joinPoint).getAnnotation(OprLog.class);
	}

	/**
	 * 反射参数对象的label来来映射对象的值内容，通过格式化字符串拼装
	 * @param joinPoint 加入对象点
	 * @param msg 格式化字符串
	 * @return 格式化完成的字符串
	 */
	private String assembleEventContentFromJoinPoint(ProceedingJoinPoint joinPoint,String msg){
		Object[] args = joinPoint.getArgs();
		Method method = getMethodFromJoinPoint(joinPoint);
		FieldLabelHandle fieldLabelHandle = new FieldLabelHandle();
		Annotation[][] annotations = method.getParameterAnnotations();
		//Annotation[参数][注解]
		for(int i =0;i<annotations.length;i++){
			Object arg = args[i];
			if(arg.getClass().equals(Long.class)){
				getFieldsByLongType(annotations[i],fieldLabelHandle,arg);
			}else{
				getFieldsByOtherType(fieldLabelHandle,arg);
			}
		}
		return fieldLabelHandle.toMsg(msg);
	}

	/**
	 * 注解类数组
	 * @param argAnnotations 参数注解数组
	 * @param fieldLabelHandle 字段处理器
	 * @param arg 参数对象
	 * @return 字段列表
	 */
	private void getFieldsByLongType(Annotation[] argAnnotations, FieldLabelHandle fieldLabelHandle, Object arg){
		List<Field> fields = new ArrayList<>();
		OprId oprId = getAnnotationFromAnnotationArray(argAnnotations);
		if(oprId!=null){
			Object obj = SpringUtil.getBean(oprId.server()).getById((Long)arg);
			if(obj!=null){
				fields = List.of(obj.getClass().getDeclaredFields());
				fieldLabelHandle.addFieldLabel(
						OprLogFieldLabelDto
								.builder()
								.fieldName(ID_NAME)
								.fieldValue(String.valueOf(arg))
								.build()
				);
				setFieldLabelInHandle(fieldLabelHandle,fields,obj);
			}
		}
	}

	/**
	 * 从非Long类型获取字段
	 * @param arg 参数
	 * @return
	 */
	private void getFieldsByOtherType(FieldLabelHandle fieldLabelHandle,Object arg){
		List<Field> fields = List.of(arg.getClass().getDeclaredFields());
		setFieldLabelInHandle(fieldLabelHandle,fields,arg);
	}

	/**
	 *
	 * @param fieldLabelHandle 字段处理器
	 * @param fields 字段
	 * @param arg 参数
	 */
	private void setFieldLabelInHandle(FieldLabelHandle fieldLabelHandle, List<Field> fields, Object arg){
		fields.forEach(field->{
			if(field.getAnnotation(OprLabel.class)!=null){
				try{
					field.setAccessible(true);
					fieldLabelHandle.addFieldLabel(
							OprLogFieldLabelDto
									.builder()
									.fieldName(field.getName())
									.fieldValue(field.get(arg).toString())
									.build()
					);
				}catch (IllegalAccessException ignored){}
			}
		});
	}


	/**
	 * 从注解数组中获取目标类型注解
	 * @param annotations 注解数组
	 * @return 注解对象实体，可以为空
	 */
	private OprId getAnnotationFromAnnotationArray(Annotation[] annotations){
		return (OprId)(Arrays.stream(annotations)
				.filter(annotation->annotation.annotationType().equals(OprId.class))
				.findFirst()
				.orElse(null));
	}


	/**
	 * 添加日志
	 * @param eventContent 日志内容
	 * @param eventName 日志标题
	 * @param request 请求对象，其中包含路由，方法名称等信息
	 */
	private void insertLog(String eventContent,String eventName, HttpServletRequest request){
		UserLogEntity userLogEntity = UserLogEntity.builder()
				.eventContent(eventContent)
				.eventName(eventName)
				.url(request.getRequestURI())
				.method(request.getMethod())
				.build();
		userLogService.save(userLogEntity);
	}

}
